#include "HashExtensible.h"
#include "math.h"
using namespace Hash;

/*
 * Inicializa el hash
 */
HashExtensible::HashExtensible(string nombre) {

	this->maximoDispersiones = 20; //debera ser constante

	Configuracion* config = Configuracion::getInstancia();

	string path = config->getPathHashExtensible() + "hash" + nombre + ".bin";
	this->tamanioCubeta = config->getTamanioBloque();
	this->cubetas = new ArchivoDeEstructurasFijas(path, this->getTamanioCubeta());

	path = config->getPathHashExtensible() + "cubetasLibres" + nombre + ".bin";
	this->cubetasLibres = new ManejadorEstructurasLibres(path);

	path = config->getPathHashExtensible() + "tablaHash" + nombre + ".bin";
	this->tabla = new Tabla(path);
	if (this->getTabla()->esNueva()) {
		Cubeta* cubeta = new Cubeta(this->getTabla()->getTamanio(), this->getTamanioCubeta());
		unsigned int numeroCubeta = this->guardarNuevaCubeta(cubeta);
		this->tabla->setElementoEn(0, numeroCubeta);
		delete cubeta;
	}
}

/*
 *  Si hay una cubeta libre, la usa
 *       sino pide una nueva cubeta y la guarda en el archivo
 */
unsigned int HashExtensible::guardarNuevaCubeta(Cubeta* cubeta) {

	int cubetaLibre = this->cubetasLibres->obtenerEstructuraLibre();
	unsigned int numeroCubeta;
	if (cubetaLibre != -1) {
		this->cubetas->modificar(cubeta, cubetaLibre);
		numeroCubeta = cubetaLibre;
	} else {
		numeroCubeta = this->cubetas->insertar(cubeta);
	}
	return numeroCubeta;
}

bool HashExtensible::modificar(string clave, unsigned int datoViejo, unsigned int datoNuevo) {

	int posicionTabla = this->aplicarFuncion(clave, this->getTabla()->getTamanio());
	unsigned int numeroCubeta = this->tabla->getElementoEn(posicionTabla);
	Cubeta* cubeta = this->obtenerCubeta(numeroCubeta);

	int posicionRegistro = cubeta->buscar(clave);
	if (posicionRegistro != -1) {

		int encontrado = cubeta->getRegistros()->at(posicionRegistro)->buscar(datoViejo);

		if (encontrado != -1) {
			cubeta->getRegistros()->at(posicionRegistro)->getOffsets()->at(encontrado) = datoNuevo;
			this->actualizarCubeta(cubeta, numeroCubeta);
			delete cubeta;
			return true;
		}
	}
	delete cubeta;
	return false;
}

/*
 *  Guarda la cubeta modificada en el archivo de cubetas
 */
void HashExtensible::actualizarCubeta(Cubeta* cubeta, unsigned int numeroCubeta) {
	this->cubetas->modificar(cubeta, numeroCubeta);
}

/**
 ** Pre: -
 ** Post: Lee una cubeta del archivo de cubetas
 **/
Cubeta* HashExtensible::obtenerCubeta(unsigned int numeroCubeta) {
	Cubeta* cubeta = new Cubeta(this->getTamanioCubeta());
	this->cubetas->leer(cubeta, numeroCubeta);
	return cubeta;
}

/*
 * Toma una clave y un tamanio de dispersion
 * Devuelve una posicion en la tabla donde ir a buscar el numero de cubeta
 */
int HashExtensible::aplicarFuncion(string clave, int tamanioDispersion) {

	int lon = clave.length();
	int dispersion = 0;
	int claveNumerica = 0;
	for (int i = 1; i < lon; i++) {
		claveNumerica += clave[i];
	}
	dispersion = claveNumerica % tamanioDispersion;
	return dispersion;

}

/*
 * Buscar Externo
 * Recibe una clave
 * Devuelve un IRegistroIndice  o
 * Un Registro con la misma clave y el offset vacío si no existia
 * El que lo recibe debe encargarse de eliminarlo
 */
IRegistroIndice* HashExtensible::buscar(string clave) {

	RegistroCubeta* resultado = this->buscarRegistro(clave);
	if (resultado != NULL) {
		return (IRegistroIndice*) resultado;
	} else {
		resultado = new RegistroCubeta();
		resultado->setClave(clave);
		return resultado;
	}
}

/*
 * Buscar Interno
 * Recibe una clave
 * Devuelve el registro guardado o NULL si no existia
 */
RegistroCubeta* HashExtensible::buscarRegistro(string clave) {

	int posicionTabla = this->aplicarFuncion(clave, this->getTabla()->getTamanio());
	unsigned int numeroCubeta = this->tabla->getElementoEn(posicionTabla);
	Cubeta* cubeta = this->obtenerCubeta(numeroCubeta);

	int posicionRegistro = cubeta->buscar(clave);
	if (posicionRegistro != -1) {
		RegistroCubeta* resultado = new RegistroCubeta(*(cubeta->getRegistros()->at(posicionRegistro)));
		delete cubeta;
		return resultado;
	} else {
		delete cubeta;
		return NULL;
	}
}

/*
 * Posiciona la cubeta nueva en la tabla
 */
int HashExtensible::getPosicionNuevaCubeta() {

	if (this->getTabla()->getTamanio() == 1) {
		return 0;
	} else {
		int resultado = 1;
		int cociente = (this->getTabla()->getTamanio()) / 2;
		while (cociente != 1) {
			cociente = cociente / 2;
			resultado++;
		}
		return resultado;
	}
}

/*
 *  Devuelve el tamanio actual de la tabla
 */
int HashExtensible::getDispersion() {
	return this->getTabla()->getTamanio();
}

Tabla* HashExtensible::getTabla() {
	return this->tabla;
}

int HashExtensible::getTamanioCubeta() const {
	return this->tamanioCubeta;
}
/*
 * Recibe el numero de cubera que se quiere liberar
 * La cubeta es liberada
 */
void HashExtensible::liberarCubeta(Cubeta* cubeta, unsigned int numeroCubeta) {

	this->cubetasLibres->insertarEstructuraLibre(numeroCubeta);
}

/*
 * Si se puede liberar
 *     Devuelve el numero de la cubeta que puede reemplazar a la que se quiere liberar
 * Si no se puede liberar
 *     Devuelve -1
 */
int HashExtensible::sePuedeLiberarLaCubeta(int posicion, int td) {

	int mov = td / 2;
	int resultado = -1;
	int arriba = posicion + mov;
	if (arriba >= this->getDispersion()) {
		arriba = arriba - this->getDispersion();
	}
	int abajo = posicion - mov;
	if (abajo < 0) {
		abajo = abajo + this->getDispersion();
	}
	if ((this->getTabla()->getElementoEn(arriba) == this->getTabla()->getElementoEn(abajo)) && (td != 1)) {
		resultado = this->getTabla()->getElementoEn(arriba);
	}
	return resultado;
}

/*
 * Eliminar un registro guardado en el hash
 */
bool HashExtensible::eliminar(string clave, unsigned int dato) {

	RegistroCubeta* registro = new RegistroCubeta(clave, dato);

	//Aplico la funcion a la clave y recupero de la tabla el numero de cubeta a leer
	int posicion = this->aplicarFuncion(registro->getClave(), this->getTabla()->getTamanio());
	unsigned int numeroCubeta = this->getTabla()->getElementoEn(posicion);
	//Obtengo la cubeta
	Cubeta* cubeta = this->obtenerCubeta(numeroCubeta);
	//Intento eliminar el registro de la cubeta
	EstadoCubeta resultado = cubeta->eliminar(registro);
	delete registro;

	switch (resultado) {
	case (OK):
		//La cubeta pudo borrar el registro (o el offset), guardo la cubeta actualizada
		this->actualizarCubeta(cubeta, numeroCubeta);
		delete cubeta;
		return true;
		break;
	case (VACIO): {
		//La cubeta quedo vacia

		//Verifico si el bloque se puede liberar
		int numeroCubetaReemplazo = this->sePuedeLiberarLaCubeta(posicion, cubeta->getTamanioDispersion());

		if (numeroCubetaReemplazo != -1) {
			//Se puede liberar la cubeta

			//Libero la cubeta guardandola en el archivo de bloques libres
			this->liberarCubeta(cubeta, numeroCubeta);
			//Leo la cubeta con la que voy a reemplazar a la liberada
			Cubeta* cubetaReemplazo = this->obtenerCubeta(numeroCubetaReemplazo);
			//Actualizo la tabla cambiando las referencias a la cubeta liberada por la cubeta de reemplazo
			//a saltos de tamanio de la dispersion de la cubeta de reemplazo
			this->getTabla()->actualizar(posicion, cubetaReemplazo->getTamanioDispersion(), numeroCubetaReemplazo);
			cubetaReemplazo->setTamanioDispersion(cubetaReemplazo->getTamanioDispersion() / 2);
			//Guardo la cubeta con el nuevo tamanio de dispersion
			this->actualizarCubeta(cubetaReemplazo, numeroCubetaReemplazo);
			//Si es posible trunca la tabla, sino la deja igual
			this->getTabla()->truncar();

			delete cubetaReemplazo;
		} else {
			// No se puede liberar la cubeta, se regraba vacia
			this->actualizarCubeta(cubeta, numeroCubeta);

		}
		delete cubeta;
		return true;
		break;
	}
	case (SINACCION):
		delete cubeta;
		return false;
		break;
	}
}

/*
 * Insertar externo, clave + offset
 */
void HashExtensible::insertar(string clave, unsigned int dato) {
	RegistroCubeta* registroNuevo = new RegistroCubeta(clave, dato);

	this->maximoDispersiones = 20;
	this->insertar(registroNuevo);

	delete registroNuevo;
}

/*
 * Insertar Interno - trata de insertar un registro en el hash
 */
void HashExtensible::insertar(RegistroCubeta* registroNuevo) {

	//Aplico la funcion a la clave y recupero de la tabla el numero de cubeta a leer
	int posicion = this->aplicarFuncion(registroNuevo->getClave(), this->getTabla()->getTamanio());
	unsigned int numeroCubeta = this->getTabla()->getElementoEn(posicion);
	//Obtengo la cubeta
	Cubeta* cubeta = this->obtenerCubeta(numeroCubeta);
	//Intento insertar el nuevo registro en la cubeta
	EstadoCubeta resultado = cubeta->insertar(*registroNuevo);
	//Evaluo los posibles estados de la cubeta al intentar insertar
	switch (resultado) {
	//La cubeta tenia lugar e inserto el registro correctamente, se actualiza en el archivo
	case (OK): {
		this->actualizarCubeta(cubeta, numeroCubeta);
		break;
	}
		//La cubeta no lo inserto porque no tenia lugar
	case (LLENO): {

		//Chequear que el td del bloque sea mayor o igual que el de la tabala para hacer esto
		if ((cubeta->getTamanioDispersion()) >= (this->getDispersion())) {

			this->getTabla()->duplicar();
			int tamanioTabla = this->getTabla()->getTamanio();

			Cubeta* nuevaCubeta = new Cubeta(tamanioTabla, this->getTamanioCubeta());
			unsigned int numeroCubetaNueva = this->guardarNuevaCubeta(nuevaCubeta);

			//Referencio a la nueva cubeta en la posicion de la tabla donde estaba la cubeta desbordada
			this->getTabla()->setElementoEn(posicion, numeroCubetaNueva);

			//Actualizo la vieja cubeta guardandola VACIA, x eso guardo la nueva en su lugar, tienen las misma caracteristicas
			this->actualizarCubeta(nuevaCubeta, numeroCubeta);
			//Redisperso los registros de la cubeta desbordada y vuelve a intentar insertar el registro

			//copio los registros uno a uno para re-dispersarlos
			vector<RegistroCubeta*>* vec = new vector<RegistroCubeta*> ;
			for (unsigned int i = 0; i < cubeta->getRegistros()->size(); i++) {
				RegistroCubeta* reg = new RegistroCubeta(*(cubeta->getRegistros()->at(i)));
				vec->push_back(reg);
			}

			//	this->reDispersar(cubeta->getRegistros(), registroNuevo);
			this->reDispersar(vec, registroNuevo);

			for (unsigned int i = 0; i < vec->size(); i++) {
				delete vec->at(i);
			}

			delete vec;
			delete nuevaCubeta;
		} else {
			/*La dispersion de la cubate es menor que el tamanio de la tabla
			 *Se cambia el tamaño de la dispersion del bloque desbordado y se redispersan sus registros
			 *Se vuelve a intentar insertar el reg que no entraba
			 */

			//Se duplica el tamanio de la dispersion del bloque desbordado
			//Se crea/toma una cubeta nueva y se le asigna el mismo tamanio de dispersion
			// que la cubeta desbordada
			Cubeta* nuevaCubeta = new Cubeta((cubeta->getTamanioDispersion() * 2), this->getTamanioCubeta());
			unsigned int numeroCubetaNueva = this->guardarNuevaCubeta(nuevaCubeta);

			//Desde la posicion donde leyo la cubeta desbordada a saltos del tamanio actual de la dispersion
			//de la cubeta desbordada, asigno en la tabla una referencia a la nueva cubeta
			this->getTabla()->actualizar(posicion, nuevaCubeta->getTamanioDispersion(), numeroCubeta);

			//Actualizo la vieja cubeta guardandola VACIA, x eso guardo la nueva en su lugar, tienen las misma caracteristicas
			this->actualizarCubeta(nuevaCubeta, numeroCubeta);

			//Redisperso los registros de la cubeta desbordada y vuelve a intentar insertar el registro
			this->reDispersar(cubeta->getRegistros(), registroNuevo);

			delete nuevaCubeta;
		}

		break;
	}
	case (SINACCION): {
		//El registro que estoy trando de insertar ya estaba guardado no se hace nada
	}
	}
	delete cubeta;
}

/*
 * Redispersa un vector de Registros e intenta volver a  insertar el registro original que no habia entrado
 */
void HashExtensible::reDispersar(vector<RegistroCubeta*>* registros, RegistroCubeta* nuevoRegistro) {

	this->maximoDispersiones = this->maximoDispersiones - 1;

	if (this->maximoDispersiones <= 0) {
		cout << "Error: MAXIMO DE DISPERSIONES";
		string error;
		cin >> error;
	} else {

		for (unsigned int i = 0; i < registros->size(); i++) {
			this->insertar(registros->at(i));
		}
		this->insertar(nuevoRegistro);
	}
}

/*
 *  True si ya existia
 *  False si no existia
 */
bool HashExtensible::existe(string clave) {

	RegistroCubeta* registro = this->buscarRegistro(clave);
	if (registro == NULL) {
		return false;
	} else {
		delete registro;
		return true;
	}

}

/*
 *Exporta la estructura a un archivo .txt
 */
void HashExtensible::exportarTxt(string nombre) {

	fstream txtFile;
	Configuracion* config = Configuracion::getInstancia();
	string path = config->getPathReportes() + nombre + ".txt";
	txtFile.open(path.c_str(), ios::out | ios::app);
	if (txtFile.bad()) {
		txtFile.open(path.c_str(), ios::out);
	}

	txtFile << "Hash extensible - Estado actual" << endl;
	txtFile << " " << endl;

	txtFile << "Tabla" << endl;
	int w = this->getTabla()->getTamanio();
	for (int i = 0; i < w; i++) {
		txtFile << this->getTabla()->getElementoEn(i) << " ";
	}
	txtFile << " " << endl;

	txtFile << " " << endl;
	txtFile << "Cubetas Libres: ";
	vector<int>* bloquesLibres = this->cubetasLibres->getListaEstructurasLibres();
	for (unsigned int i = 0; i < bloquesLibres->size(); i++) {
		txtFile << " (" << bloquesLibres->at(i) << ") ";
	}
	delete bloquesLibres;

	int cant = this->cubetas->getCantidadDeEstructuras();
	txtFile << " " << endl;
	txtFile << " " << endl;
	txtFile << "Cubetas: " << endl;
	txtFile << endl;
	for (int i = 0; i < cant; i++) {
		Cubeta* cubeta = this->obtenerCubeta(i);
		if (cubeta != NULL) {
			txtFile << "(" << i << ") (" << cubeta->getTamanioDispersion() << ") (" << cubeta->getBytesLibres() << ") ";
			vector<RegistroCubeta*>* registros = cubeta->getRegistros();
			for (unsigned int t = 0; t < registros->size(); t++) {
				txtFile << endl << "Clave: " << registros->at(t)->getClave() << " " << endl;
				txtFile << "Offsets: ";
				for (unsigned int j = 0; j < registros->at(t)->getOffsets()->size(); j++) {
					txtFile << registros->at(t)->getOffsets()->at(j) << " | ";
				}
			}
			txtFile << " " << endl << " * " << endl;
			delete cubeta;
		}
	}
	txtFile << " " << endl;
	txtFile << " " << endl;
	txtFile << "**********" << endl;
	txtFile.close();
}

vector<unsigned int>* HashExtensible::getTodosLosDatos() {

	vector<unsigned int>* claves = new vector<unsigned int> ();
	unsigned int cant = this->cubetas->getCantidadDeEstructuras();
	vector<int>* bloquesLibres = this->cubetasLibres->getListaEstructurasLibres();

	for (unsigned int i = 0; i < cant; i++) {

		// limpio a las cubetas libres
		bool esUnBloqueLibre = false;
		for (unsigned int j; j < bloquesLibres->size(); j++) {
			if (i == bloquesLibres->at(j)) {
				esUnBloqueLibre = true;
				break;
			}
		}

		if (!esUnBloqueLibre) {
			Cubeta* cubeta = this->obtenerCubeta(i);

			// recorro los registros de una cubeta y por cada uno les tomo los registros
			for (unsigned int k = 0; k < cubeta->getRegistros()->size(); k++) {
				for (unsigned int l = 0; l < cubeta->getRegistros()->at(k)->getOffsets()->size(); l++) {
					claves->push_back(cubeta->getRegistros()->at(k)->getOffsets()->at(l));
				}
			}

			delete cubeta;
		}
	}

	delete bloquesLibres;
	return claves;
}

HashExtensible::~HashExtensible() {
	delete this->cubetas;
	delete this->cubetasLibres;
	delete this->tabla;

}
